<?php
/**
 *  Form elements can be of 2 kinds: standard and custom.
 *  Standard form elements are those with class names Yz_Form_Element_*.
 *  These are not subject to class name resolution.
 *
 *  @todo default action derived from module name
 *  @todo element classes must be inheritable
 *
 */
abstract class Yz_Form_Abstract
    extends Yz_Object
    implements Yz_Form_Interface
{

    /** predefined elements
     *
     */
    const ELEMENT_CHECKBOX = 'Yz_Form_Element_Checkbox';
    const ELEMENT_DATE = 'Yz_Form_Element_Date';
    const ELEMENT_INPUT = 'Yz_Form_Element_Input';
    const ELEMENT_SELECT = 'Yz_Form_Element_Select';
    const ELEMENT_TEXTAREA = 'Yz_Form_Element_Textarea';
    const ELEMENT_TINYMCE = 'Yz_Form_Element_TinyMCE';
    const ELEMENT_UPLOADFILE = 'Yz_Form_Element_UploadFile';
    const ELEMENT_UPLOADIMAGE = 'Yz_Form_Element_UploadImage';

    /** predefined renderer and parser
     *
     */
    const RENDERER_DEFAULT = 'Yz_Form_Renderer_Default';
    const PARSER_DEFAULT = 'Yz_Form_Parser_Default';

    /**
     *  @var string
     */
    protected $name;
    /**
     *  @var string
     */
    protected $action = "";
    /**
     *  @var string
     */
    protected $method = 'POST';


    /** The form's elements
     *  @var array
     */
    protected $elements = array();

    /** The storage for assigned variables
     *  @var array
     */
    protected $values;

    /**
     *  @var array
     */
    protected $parseErrors = array();

    /**
     *  @var Yz_Module_Interface
     */
    protected $module;

    /**
     *  @var Yz_Config
     */
    protected $config;

    /**
     *  @var ReflectionClass
     */
    protected $reflection;
    /** set custom renderer
     *  @var null
     */
    protected $renderer;
    /** set custom parser
     *  @var null
     */
    protected $parser;



    /****************************
     *      Public methods      *
     ****************************/


    /**
     *
     */
    public function __construct( Yz_Module_Interface $module, $elements=null )
    {
        debug();
        $this->name = lcfirst($this->shortClassName());
        $this->reflection = new ReflectionClass($this);
        $this->module = $module;

        // now create elements
        if (null !== $elements) {
            $this->assignElements( $elements );
        }
    }


    /**
     *  @param array-type elements: [name: params]
     *      Params are passed to element constructor
     *  @return chainable
     */
    public function assignElements( $elements )
    {
        debug();
        $elements = Yz_Hash::valueOf($elements);
        foreach ($elements as $name => $params) {
            $el = $this->createElement($name, $params);
            if (null !== $el) {
                $this->elements[$name] = $el;
            }
        }
        return $this;
    }

    /**
     *  @return string form's class name
     */
    final public function __toString()
    {
        $c = $this->className();
        return "Object<{$c}>('{$this->name}')";
    }

    /** get the module the form belongs to
     *  Also a getter may be used: x = $form->module
     *  @return Yz_Module_Interface
     */
    final public function getModule()
    {
        return $this->module;
    }

    /**
     *  Also a getter may be used: x = $form->name
     *
     */
    final public function getName()
    {
        return $this->name;
    }

    /**
     *  Also a setter may be used: $form->name
     *
     */
    final public function setName($name)
    {
        $this->name = Yz_String::test($name);
        return $this;
    }

    /**
     *  Also a getter may be used: x = $form->action
     *
     */
    final public function getAction()
    {
        return $this->action;
    }

    /**
     *  Also a setter may be used: $form->action
     *
     */
    final public function setAction($action)
    {
        $this->action = Yz_String::test($action);
        return $this;
    }

    /**
     *  Also a getter may be used: x = $form->action
     *
     */
    final public function getMethod()
    {
        return $this->method;
    }

    /**
     *  Also a setter may be used: $form->method
     *
     */
    final public function setMethod($method)
    {
        $this->method = Yz_String::test($method);
        return $this;
    }

    /**
     *  return copy of elements array
     *  @return Yz_Hash
     */
    final public function getElements()
    {
        return new Yz_Hash($this->elements);
    }

    /**
     *  Also a getter may be used: x = $form->values
     *  @return array
     */
    final public function getValues()
    {
        debug();
        $values = array();
        foreach ($this->elements as $elName => $el) {
            $values[$elName] = $el->getValue();
        }
        return $values;
    }

    /**
     *  Also a setter may be used: $form->values = x
     *
     */
    public function assignValues($values)
    {
        debug();
        if (is_array($values)) {
            $values = new Yz_Hash($values);
        } else if ( ! ($values instanceof Yz_Class) ) {
            $values = new Yz_Hash();
        }
        foreach ($this->elements as $elName => $el) {
            $el->findValue($values);
        }
        return $this;
    }

    /**
     *  A renderer defines:
     *  - the visual wrapping ("design") of form elements
     *  - the order of elements
     *  - the relationships among elements (eg. If A=x, skip B)
     *  - the SUBMIT element
     */
    public function render(Yz_Template_Interface $template)
    {
        debug();
        $class = $this->identifyClass('Renderer', $this->renderer);
        $r = new $class($this);
        return $r->render($template);
    }


    /**
     *  A parser defines:
     *  - the "requiredness" of elements
     *  - the relationships among elements (eg. If A=x, skip B)
     *  - external parameters which affect the parsing (resubmission, delays, etc)
     */
    public function parse($values)
    {
        debug();
        $class = $this->identifyClass('Parser', $this->parser);
        $p = new $class($this);
        // TODO collect values and errors from elements
        $res = $p->parse($this, $values);
        if (false === $res) {
            $this->parseErrors = $p->getErrors();
        }
        return $this;
    }



    /****************************
     *      Protected methods   *
     ****************************/

    /** create form element with name, using optional configuration:
     *      null        ->
     *      string      -> element type name
     *      Yz_Class    -> configuration object, with formElementType property for element type
     *  @param string $name
     *  @param mixed $config
     *  @return Yz_Form_Element_Interface
     */
    protected function createElement($name, $config)
    {
        if ($config instanceof Yz_Class) {
            $type = $config->formElementType;
            if (false === $type) {
                return null;
            }
        } else if (is_string($config)) {
            $type = $config;
            $config = new Yz_Hash();
        } else {
            $type = 'Input';
            $config = new Yz_Hash();
        }

        $class = $this->identifyClass('Element', $type);
        debug("Creating element '$name' type '$class'");
        return new $class($this, $name, $config);
    }


    /** identify the class name of component (element, parser, renderer)
     *      either by a class constant or by module.resolve
     *  @param string $libType
     *  @param string|null $libName [Default]
     *  @return string class name
     *  @throw InvalidArgument if $libName is not a valid identifier
     */
    protected function identifyClass($libType, $libName)
    {
        if (null === $libName) {
            $libName = 'Default';
        } else {
            $libName = Yz_String::testIdentifier($libName);
        }
        $const = strtoupper($libType . '_' . $libName);
        $refl = $this->reflection;
        if ($refl->hasConstant($const)) {
            $class = $refl->getConstant($const);
            Yz::loadClass($class);
        } else {
            $libName = ucfirst($libName);
            $class = $this->module->resolveClass("Form_{$libType}_{$libName}");
        }
        return $class;
    }

}
